package com.snts.synchronization.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.snts.synchronization.Utils;
import com.snts.synchronization.core.Change;
import com.snts.synchronization.core.ChangeColumn;
import com.snts.synchronization.core.Column;
import com.snts.synchronization.core.ExternalTable;
import com.snts.synchronization.core.SyncTable;
import com.snts.synchronization.core.Synchronization;
import com.snts.synchronization.core.UUID;
import com.snts.synchronization.core.Change.Type;

public class ChangesParser {

	private static final Log log = LogFactory.getLog(ChangesParser.class);

	private static final String TABLE = "table";
	private static final String ROW_ID = "rowId";
	private static final Object SCHEMA = "schema";

	private List<Change> changes;
	private Change change;
	private ExternalTable externalTable;
	private Synchronization synchronization;

	private Map<String, String> attrs = null;

	private boolean literal;

	/**
	 * @param synchronization
	 */
	public ChangesParser(Synchronization synchronization) {
		super();
		this.synchronization = synchronization;
	}

	private static abstract class CharacterProcessor<T extends Object> {

		protected StringBuffer buf = new StringBuffer();

		public String getStringValue() {
			return buf.toString();
		}

		public abstract boolean processCharacter(Reader r, char ch) throws IOException, Exception;

	}

	private static class MyPushbackReader extends PushbackReader {

		public MyPushbackReader(Reader in, char ch) {
			super(in, 1);
			try {
				this.unread(ch);
			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException("Unexpected exception during unread!", e);
			}
		}
		
	}
	
	private static class ReaderWrapper extends Reader {

		private char firstCharacter;
		private Reader delegate;
		
		/**
		 * @param firstCharacter
		 * @param delegate
		 */
		public ReaderWrapper(Reader delegate, char firstCharacter) {
			super();
			this.firstCharacter = firstCharacter;
			this.delegate = delegate;
		}

		@Override
		public int read(char[] cbuf, int off, int len) throws IOException {
			log.debug("ReaderWrapper.read()");
			if(firstCharacter != 0){
				cbuf[off++] = firstCharacter;
			}
			int count = delegate.read(cbuf, off, len-1);
			if(firstCharacter != 0){
				if(count == -1){
					count = 1;
				} else {
					count ++;
				}
			}
			firstCharacter = 0;
			return count;
		}

		@Override
		public void close() throws IOException {
			delegate.close();
		}

	}

	private String processCharacters(Reader r, CharacterProcessor chpr) throws Exception {
		int ch;
		while ((ch = r.read()) != -1){
			if(!literal && ch == '#'){
				processCharacters(r, new CharacterProcessor<Object>() {

					@Override
					public boolean processCharacter(Reader r, char ch) throws IOException, Exception {
						if(ch != '\n'){
							return true;
						}
						return false;
					}
				
				});
				continue;
			}
			log.trace("processCharacters: " + (char) ch);
			if(!chpr.processCharacter(r, (char) ch)){
				break;
			}
		}
		return chpr.getStringValue();
	}

	public List<Change> parse(Reader r) throws Exception {
		changes = new ArrayList<Change>();
		if (!(r instanceof BufferedReader)) {
			r = new BufferedReader(r);
		}
		processCharacters(r, new CharacterProcessor() {
			@Override
			public boolean processCharacter(Reader r, char ch) throws Exception {

				if(ch != '\n'){
					r = new MyPushbackReader(r, ch);
				}
				String updateType = readUpdateType(r);
				log.debug("updateType: " + updateType);
				if(!"INSERT".equals(updateType) && !"UPDATE".equals(updateType)){
					return false;
				}
				Change.Type type = "INSERT".equals(updateType) ? Change.Type.INSERT : Change.Type.UPDATE;
				change = new Change(type);
				externalTable = new ExternalTable();
				attrs = new HashMap<String, String>();

				while(readAttribute(r));

				log.debug("Attrs:");

				for(Entry entry : attrs.entrySet()){
					log.debug(entry.getKey() + "=>" + entry.getValue() + " ");
				}

				String schema = attrs.get(SCHEMA);
				if(schema == null || "".equals(schema)){
					schema="public";
				}
				String table = attrs.get(TABLE);

				externalTable.setSchema(schema);
				externalTable.setName(table);
				SyncTable syncTable = synchronization.getTableByExternalName(externalTable);

				if (syncTable != null) {

					change.setTable(syncTable);
					change.setRowId(new UUID(attrs.get(ROW_ID)));

					for (Entry<String, String> entry : attrs.entrySet()) {
						String attrName = entry.getKey();
						String attrValue = entry.getValue();
						Column column = syncTable.getColumnByExternalName(attrName);
						if(column != null){
							ChangeColumn changeColumn = change.addColumn(column);
							changeColumn.setAsString(attrValue);
						}
					}

					changes.add(change);
					return true;

				} else {
					log.error("Table \"" + externalTable + "\" not found! Simply ignoring!");
					return true;
				}

			}
		});
		return changes;
	}
	
	private String readUpdateType(Reader r) throws Exception {
		return processCharacters(r, new CharacterProcessor() {
			@Override
			public boolean processCharacter(Reader r, char ch) throws IOException, Exception {
				if (ch != '(') {
					buf.append(ch);
					log.debug("Current buffer: " + buf.toString());
					return true;
				} else 
					return false;
				}
		}).trim();
	}

	private boolean readAttribute(Reader r) throws Exception {
		String attrName = readAttrName(r);
		StringBuffer buf = new StringBuffer();
		int ch;
		boolean last = false;
		while ((ch = r.read()) != -1) {
			log.debug("readAttribute: character - " + (char) ch);
			if (ch == ')') {
				last = true;
				break;
			} else if (ch == ',') {
				last = false;
				break;
			} else if (ch == '\'') {
				literal = true;
				String literal = processCharacters(r, new CharacterProcessor<Object>() {
					@Override
					public boolean processCharacter(Reader r, char ch) throws Exception {
						if (ch == '\\') { // append next character into
											// StringBuffer as is
							int next = (char) r.read();
							if (next != -1) {
								buf.append(next);
							}
 							return true;
						} else if (ch == '\'') {
							ChangesParser.this.literal = false;
							return false;
						} else {
							buf.append(ch);
							return true;
						}
					}
				});
				log.debug("Literal: " + literal);
				buf.append(literal);
			} else {
				buf.append((char) ch);
			}
		}
		String attrValue = buf.toString().trim();
		attrs.put(attrName, attrValue);
		return ! last;
	}

	private String readAttrName(Reader r) throws Exception {
		return processCharacters(r, new CharacterProcessor() {
			@Override
			public boolean processCharacter(Reader r, char ch) throws IOException, Exception {
				if(ch == '=')
				{
					return false;
				}
				else 
				{
					buf.append(ch);
					return true;
				} 
			}
		}).trim();
	}

	public static void main(String[] args) throws Exception {
		ChangesParser changesParser = new ChangesParser(Utils.createSynchronization());
		List<Change> changes = changesParser.parse(new InputStreamReader(ChangesParser.class.getResourceAsStream("Changes.txt")));
		for(Change change : changes){
			System.out.print(change.getType() == Type.UPDATE ? "UPDATE" : "INSERT");
			System.out.print("(schema=" + change.getTable().getExternalTable().getSchema() + ", table=" + change.getTable().getExternalTable().getName() 
					          + ", rowId=" + change.getRowId().toString());
			for(ChangeColumn changeColumn : change.getColumns()){
				System.out.print(", " + changeColumn.getColumn().getName() + "=" + changeColumn.getValue().toString());
			}
			System.out.println(")");
		}
/*		
		String str="PDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE,UPDATE, UPDATE, UPDATE";
		ReaderWrapper rw = new ReaderWrapper(new StringReader(str), 'F');
		char[] cbuf = new char[1024];
		int count;
		StringBuffer buf = new StringBuffer();
		while((count = rw.read(cbuf)) != -1){
			for(int i = 0; i < count; i ++){
//				System.out.print(cbuf[i]);
				buf.append(cbuf[i]);
			}			
		}
		System.out.println();
		System.out.println(('F' + str).equals(buf.toString()));
*/
	}

}
